%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc. All Rights Reserved.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% Basic XML and JSON output functions, used in conjunction with web.erl
%%% to support simple browser views.
%%%
%%% <em>These functions are not complete in respect of all escaping and quoting
%%% issues at this time.</em>
%%%
%%% Both the JSON and XML generators operate on the same structured format,
%%% so that you can use JSON or XML interchangeably. The `struct' format is:
%%% <ul>
%%% <li>`{atom()}' - a simple atom</li>
%%% <li>`{atom(), list(attribute()}' - an atom with a set of key/value
%%% tuples</li>
%%% <li>`{atom(), list(attribute()), list(struct())}' - an atom with both
%%% attributes and a list of (recursive) `struct' content.</li>
%%% </ul>
%%% ===Example 1===
%%% Simple content:
%%% ```
%%%   Struct = {person, [{age, 46}, {name, jacoby}]},
%%%   Json = struct_to_json(Struct),
%%%   Xml = struct_to_xml(Struct), ...
%%% '''
%%% <p/>results in both `Json' and `Xml' holding deep(ish) lists which, when
%%% flattened using `lists:flatten()' or `io:format()', look like this:
%%% <p/>JSON:
%%% ```
%%%   {person : { age : 46, name : "jacoby"}}
%%% '''
%%% <p/>XML:
%%% ```
%%%   <person age='46' name='jacoby'/>
%%% '''
%%% ===Example 2===
%%% No content, ie just an {atom} in the struct:
%%% ```
%%%   Struct = {person}, ...
%%% '''
%%% <p/>results in JSON:
%%% ```
%%%   {person : true}
%%% '''
%%% <p/>and XML
%%% ```
%%%   <person/>
%%% '''
%%% ===Example 3===
%%% Complex content:
%%% ```
%%%     Struct = {person, [{age, 46}, {name, "jacoby"}],
%%%                 [{dependent, [{sex, female}, {age, 9}]},
%%%                  {dependent, [{sex, "male"}, {age, 1}]}]}, ...
%%% '''
%%% <p/>results in JSON:
%%% ```
%%%   { person : { age : 46, name : "jacoby"},
%%%     contents : [
%%%        { dependent : { sex : "female", age : 9}},
%%%        { dependent : { sex : "male", age : 1}}
%%%      ]
%%%   }
%%% '''
%%% <p/>and XML:
%%% ```
%%%   <person age='46' name='jacoby'>
%%%     <dependent sex='female' age='9'/>
%%%     <dependent sex='male' age='1'/>
%%%   </person>
%%% '''
%%% <p/>In the complex JSON output, the generated object literal has a second
%%% array property `contents'. It would have been nicer to generate an array
%%% literal with the named attributes on it (which is perfectly possible in
%%% Javascript) but that is not supported in the JSON object literal subset.
%%%
%%% @todo(jacoby) Complete XML and JSON escaping plus tests.
-module(xj).

%%%---------------------------------------------------------------------------
%%% Constants
%%%---------------------------------------------------------------------------

%% Common constants
-define(SINGLE_QUOTE, $').
-define(DOUBLE_QUOTE, $").

%% XML constants
-define(QUOTE, ?SINGLE_QUOTE).
-define(SPACE, $\ ).
-define(GT, $>).
-define(LT, $<).
-define(SLASH, $/).
-define(EQUALS, $=).

%% JSON constants
-define(CONTENTS_PROP, "\"contents\"").
-define(TEXT_PROP, "\"text\"").
-define(OPEN_SQ, $[).
-define(CLOSE_SQ, $]).
-define(OPEN_CU, ${).
-define(CLOSE_CU, $}).
-define(COMMA, $,).
-define(TRUE, "true").
-define(COLON, $:).

%%%---------------------------------------------------------------------------
%%% Types

%% @type xml() = list(string() | xml()). Both JSON and XML return types are
%% deep lists of strings. This is fine in Erlang, whose stream
%% output functions automatically flatten deep lists. Alternatively, the user
%% can use `lists:flatten/1' explicitly.
-type(xml() :: list(string() | xml())).

%% @type json() = list(string() | json()). Both JSON and XML return types are
%% deep lists of strings. This is fine in Erlang, whose stream
%% output functions automatically flatten deep lists. Alternatively, the user
%% can use `lists:flatten/1' explicitly.
-type(json() :: list(string() | json())).

%%%---------------------------------------------------------------------------
%%% Exports
%%%---------------------------------------------------------------------------
-export([struct_to_json/1, structs_to_json/1]).
-export([struct_to_xml/1, structs_to_xml/1]).

%%%---------------------------------------------------------------------------
%%% JSON Generators
%%%---------------------------------------------------------------------------

%% @doc Returns the {@link json} output version of the supplied list of
%% {@link struct()}.
%% @spec structs_to_json(list(struct())) -> json()
structs_to_json([]) -> [];
structs_to_json([Struct]) -> struct_to_json(Struct);
structs_to_json([Struct | Structs]) ->
  [struct_to_json(Struct), ?COMMA | structs_to_json(Structs)].

%% @doc Returns the {@link json()} output version of the supplied
%% {@link struct()}.
%% @spec struct_to_json(struct()) -> json()
struct_to_json(Text) when is_list(Text) ->
  [?OPEN_CU, ?TEXT_PROP, ?COLON,
   ?DOUBLE_QUOTE, Text, ?DOUBLE_QUOTE,
   ?CLOSE_CU];
struct_to_json({Elem}) ->
  [?OPEN_CU, json_prop_name(Elem), ?COLON, "true", ?CLOSE_CU];
struct_to_json({Elem, Attributes})
  when is_list(Attributes) ->
  [?OPEN_CU, json_prop_name(Elem), ?COLON, ?OPEN_CU,
   attrs_to_json(Attributes),
   ?CLOSE_CU, ?CLOSE_CU];
struct_to_json({Elem, Attributes, Contents})
  when is_list(Attributes), is_list(Contents) ->
  [?OPEN_CU, json_prop_name(Elem), ?COLON, ?OPEN_CU,
   attrs_to_json(Attributes),
   ?CLOSE_CU, ?COMMA,
   ?CONTENTS_PROP, ?COLON, ?OPEN_SQ,
   structs_to_json(Contents),
   ?CLOSE_SQ, ?CLOSE_CU].

%% JSON named property list.
attrs_to_json([]) -> [];
attrs_to_json([Last]) -> attr_to_json(Last);
attrs_to_json([Next | Rest]) ->
  [attr_to_json(Next), ?COMMA | attrs_to_json(Rest)].

%% JSON named property : value pairs, correctly quoted.
attr_to_json({Name, Atom}) when is_atom(Atom) ->
  [json_prop_name(Name), ?COLON,
   ?DOUBLE_QUOTE, atom_to_list(Atom), ?DOUBLE_QUOTE];
attr_to_json({Name, Integer}) when is_integer(Integer) ->
  [json_prop_name(Name), ?COLON, integer_to_list(Integer)];
attr_to_json({Name, String}) when is_list(String) ->
  [json_prop_name(Name), ?COLON, ?DOUBLE_QUOTE, String, ?DOUBLE_QUOTE];
attr_to_json({Name, Float}) when is_float(Float) ->
  [json_prop_name(Name), ?COLON, float_to_list(Float)];
attr_to_json({Name, Pid}) when is_pid(Pid) ->
  [json_prop_name(Name), ?COLON, ?DOUBLE_QUOTE, pid_to_list(Pid), ?DOUBLE_QUOTE];
attr_to_json({Name, Tuple}) when is_tuple(Tuple) ->
  [json_prop_name(Name), ?COLON,
   ?DOUBLE_QUOTE, io_lib:format("~w", [Tuple]), ?DOUBLE_QUOTE];
attr_to_json({Name, true}) ->
  [json_prop_name(Name), ?COLON, "true"];
attr_to_json({Name, false}) ->
  [json_prop_name(Name), ?COLON, "false"].

%% JSON property names
json_prop_name(Atom) when is_atom(Atom) -> [$", atom_to_list(Atom), $"];
json_prop_name(String) when is_list(String) -> [$", String, $"].

%%%---------------------------------------------------------------------------
%%% XML Generators
%%%---------------------------------------------------------------------------

%% @doc Returns the {@link xml()} output version of the supplied list of
%% {@link struct()}.
%% @spec structs_to_xml(list(struct())) -> xml()
structs_to_xml(Structs) ->
  [struct_to_xml(Struct) || Struct <- Structs].

%% @doc Returns the {@link xml()} output version of the supplied {@link struct}.
%%
%% XML CDATA section.
%% @spec struct_to_xml(struct()) -> xml()
struct_to_xml(Text) when is_list(Text) ->
  ["<![CDATA[", Text | "]]>"];

%% XML Element
struct_to_xml({Elem}) ->
  [?LT, xml_prop_name(Elem), ?SLASH, ?GT];

%% XML Element with attributes
struct_to_xml({Elem, Attributes})
  when is_list(Attributes) ->
  [?LT, xml_prop_name(Elem), attrs_to_xml(Attributes), ?SLASH, ?GT];

%% XML Element with attributes (empty list if none) and content.
struct_to_xml({Elem, Attributes, Contents})
  when is_list(Attributes), is_list(Contents) ->
  [?LT, xml_prop_name(Elem), attrs_to_xml(Attributes), ?GT,
   structs_to_xml(Contents),
   ?LT, ?SLASH, xml_prop_name(Elem), ?GT].

%% XML Attributes.
attrs_to_xml([]) -> [];
attrs_to_xml([{Name, Value} | Rest]) ->
  [?SPACE,
   xml_prop_name(Name), ?EQUALS, ?QUOTE, value_to_xml(Value), ?QUOTE
     | attrs_to_xml(Rest)].

%% XML property (element and attribute) names
xml_prop_name(Atom) when is_atom(Atom) -> atom_to_list(Atom);
xml_prop_name(String) when is_list(String) -> String.

%% XML Attribute values.
value_to_xml(Atom) when is_atom(Atom) -> escape_xml(atom_to_list(Atom));
value_to_xml(String) when is_list(String) -> escape_xml(String);
value_to_xml(Integer) when is_integer(Integer) -> integer_to_list(Integer);
value_to_xml(Float) when is_float(Float) -> float_to_list(Float);
value_to_xml(Pid) when is_pid(Pid) -> escape_xml(pid_to_list(Pid));
value_to_xml(Tuple) when is_tuple(Tuple) ->
  escape_xml(escape_xml(io_lib:format("~w", [Tuple]))).

%% @doc Escapes XML attribute values and PCDATA (Parsed Character Data).
%% This is not complete but serves our purpose.
%% @spec escape_xml(string()) -> string()
escape_xml([]) -> [];
escape_xml([$< | Rest]) -> ["&lt;" | escape_xml(Rest)];
escape_xml([$> | Rest]) -> ["&gt;" | escape_xml(Rest)];
escape_xml([$& | Rest]) -> ["&amp;" | escape_xml(Rest)];
escape_xml([Other | Rest]) -> [Other | escape_xml(Rest)].
